package com.fm.magic.database;

import com.fm.base.database.DBCursor;
import com.fm.base.database.DBDataFilter;
import com.fm.base.database.DBDomainService;
import com.fm.base.database.DBResult;
import com.fm.base.database.DBSort;
import com.fm.base.database.DBSortField;
import com.fm.base.database.DBSortOrder;
import com.fm.base.database.domain.DBDocument;
import com.fm.base.database.domain.DBList;
import com.fm.base.util.Pair;
import com.fm.magic.Factory;

import java.util.Comparator;
import java.util.Date;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;

public class DBMonitor {
    static final String IMAGE = "image";
    static final String OPTIONS = "options";
    static final String TIME = "time";
    static final String OPTION = "option";
    static final String DETAIL = "detail";

    public static final String OPTION_CREATE = "create";
    public static final String OPTION_DELETE = "delete";
    public static final String OPTION_INSERT = "insert";
    public static final String OPTION_UPDATE = "update";
    public static final String OPTION_ADDKEY = "addkey";
    public static final String OPTION_DELKEY = "delkey";

    private int create; // 不曾有过的数据
    private int insert; // 曾经有过的数据
    private int delete; // 被删除掉的数据
    private int update; // 发生改变的数据
    private int addKey; // 增加了键的数据
    private int delKey; // 减少了键的数据

    private Pair<DBDocument, Boolean> loopToTrack(
        Iterator<DBDocument> restInToTrack,
        DBDocument historyCur,
        String idField,
        Comparator<Object> idComparator,
        DBDomainService<DBDocument> historyDomain,
        long time
    ) {
        DBDocument toTrackCur;
        Object id = historyCur.get(idField);
        while (restInToTrack.hasNext()) {
            toTrackCur = restInToTrack.next();
            int sub = idComparator.compare(id, toTrackCur.get(idField));
            if (sub > 0) {
                logCreate(historyDomain, toTrackCur, idField, time);
            }
            if (sub == 0) {
                logOthers(historyDomain, toTrackCur, historyCur, idField, time);
                return new Pair<>(null, Boolean.TRUE);
            }
            if (sub < 0) {
                return new Pair<>(toTrackCur, Boolean.FALSE);
            }
        }
        return new Pair<>(null, null);
    }

    private Pair<DBDocument, Boolean> loopHistory(
        Iterator<DBDocument> restInHistory,
        DBDocument toTrackCur,
        String idField,
        Comparator<Object> idComparator,
        DBDomainService<DBDocument> historyDomain,
        long time
    ) {
        DBDocument historyCur;
        Object id = toTrackCur.get(idField);
        while (restInHistory.hasNext()) {
            historyCur = restInHistory.next();
            int sub = idComparator.compare(id, historyCur.get(idField));
            if (sub > 0) {
                logDelete(historyDomain, historyCur, idField, time);
            }
            if (sub == 0) {
                logOthers(historyDomain, toTrackCur, historyCur, idField, time);
                return new Pair<>(null, Boolean.TRUE);
            }
            if (sub < 0) {
                return new Pair<>(historyCur, Boolean.FALSE);
            }
        }
        return new Pair<>(null, null);
    }

    protected void track(DBDomainService<DBDocument> toTrackDomain, DBDomainService<DBDocument> historyDomain, String idField, Comparator<Object> idComparator) {
        System.out.print(toTrackDomain.getName() + " begin:" + new Date().toString());
        create = 0;
        insert = 0;
        delete = 0;
        update = 0;
        addKey = 0;
        delKey = 0;

        DBResult<DBDocument> allInHistoryData = historyDomain.cursor(null).sort(new DBSort().append(new DBSortField(idField, DBSortOrder.ASC)));
        DBResult<DBDocument> allInToTrackData = toTrackDomain.cursor(null).sort(new DBSort().append(new DBSortField(idField, DBSortOrder.ASC)));
        try (DBCursor<DBDocument> allInHistory = allInHistoryData.getCursor();
             DBCursor<DBDocument> allInToTrack = allInToTrackData.getCursor()) {
            DBDocument historyCur = null;
            DBDocument toTrackCur = null;

            while (true) {
                if (historyCur == null) {
                    if (allInHistory.hasNext()) {
                        historyCur = allInHistory.next();
                    }
                }
                if (toTrackCur == null) {
                    if (allInToTrack.hasNext()) {
                        toTrackCur = allInToTrack.next();
                    }
                }

                long time = System.currentTimeMillis();

                if (historyCur != null && toTrackCur != null) {
                    int sub = idComparator.compare(historyCur.get(idField), toTrackCur.get(idField));
                    if (sub == 0) { // 一样大
                        logOthers(historyDomain, toTrackCur, historyCur, idField, time);
                        historyCur = null;
                        toTrackCur = null;
                        continue;
                    }
                    if (sub > 0) { // his大
                        logCreate(historyDomain, toTrackCur, idField, time);
                        Pair<DBDocument, Boolean> result = loopToTrack(allInToTrack, historyCur, idField, idComparator, historyDomain, time);
                        if (result.right == Boolean.TRUE) {
                            historyCur = null;
                            toTrackCur = null;
                        } else if (result.right == Boolean.FALSE) {
                            logDelete(historyDomain, historyCur, idField, time);
                            historyCur = null;
                            toTrackCur = result.getLeft();
                        } else {
                            logDelete(historyDomain, historyCur, idField, time);
                            historyCur = null;
                            toTrackCur = null;
                        }
                    }
                    if (sub < 0) { // track大
                        logDelete(historyDomain, historyCur, idField, time);
                        Pair<DBDocument, Boolean> result = loopHistory(allInHistory, toTrackCur, idField, idComparator, historyDomain, time);
                        if (result.right == Boolean.TRUE) {
                            historyCur = null;
                            toTrackCur = null;
                        } else if (result.right == Boolean.FALSE) {
                            logCreate(historyDomain, toTrackCur, idField, time);
                            historyCur = result.getLeft();
                            toTrackCur = null;
                        } else {
                            logCreate(historyDomain, toTrackCur, idField, time);
                            historyCur = null;
                            toTrackCur = null;
                        }
                    }
                } else if (historyCur != null) { // toTrackCur == null
                    logDelete(historyDomain, historyCur, idField, time);
                    while (allInHistory.hasNext()) {// 遍历剩余的allInHistory
                        historyCur = allInHistory.next();
                        logDelete(historyDomain, historyCur, idField, time);
                    }
                    historyCur = null;
                } else if (toTrackCur != null) { // historyCur == null
                    logCreate(historyDomain, toTrackCur, idField, time);
                    while (allInToTrack.hasNext()) {// 遍历剩余的allInToTrack
                        toTrackCur = allInToTrack.next();
                        logCreate(historyDomain, toTrackCur, idField, time);
                    }
                    toTrackCur = null;
                } else {
                    break;
                }
            }
        }
        System.out.println(" --> end:" + new Date().toString());
    }

    private void logCreate(DBDomainService<DBDocument> domain, DBDocument document, String idField, long time) {
        DBDocument history = Factory.createDBSortedDocument();
        history.put(idField, document.get(idField));
        history.put(IMAGE, document);
        history.put(OPTIONS, Factory.createDBList()
            .append(Factory.createDBSortedDocument()
                .append(TIME, time)
                .append(OPTION, OPTION_CREATE)
            )
        );
        domain.insert(history);
        create++;
    }

    /**
     * 不一定是真的发生了删除操作，有可能只是
     */
    private void logDelete(DBDomainService<DBDocument> domain, DBDocument history, String idField, long time) {
        DBList options = history.getDBList(OPTIONS);
        DBDocument lastOption = options.asList(DBDocument.class).get(options.size() - 1);
        if (!OPTION_DELETE.equals(lastOption.getString(OPTION))) {
            DBDocument option = Factory.createDBSortedDocument()
                .append(TIME, time)
                .append(OPTION, OPTION_DELETE)
                .append(DETAIL, history.get(IMAGE));
            options.add(option);
            history.put(IMAGE, null);
            domain.save(DBDataFilter.equal(idField, history.get(idField)), history);
            delete++;
        }
    }

    private void logOthers(DBDomainService<DBDocument> domain, DBDocument toTrack, DBDocument history, String idField, long time) {
        DBDocument image = history.getDBDocument(IMAGE);

        if (image == null) {
            logInsert(domain, toTrack, history, idField, time);
        } else {
            image = image.clone();
            DBDocument update = Factory.createDBSortedDocument();
            DBDocument addKey = Factory.createDBSortedDocument();
            DBDocument delKey = Factory.createDBSortedDocument();
            for (Map.Entry<String, Object> entry : toTrack.entrySet()) {
                if (image.containsKey(entry.getKey())) { // 可能发生更新的字段
                    Object imageValue = image.remove(entry.getKey());
                    if (!Objects.equals(entry.getValue(), imageValue)) {
                        update.put(entry.getKey(), imageValue);
                    }
                } else { // 新增加的字段
                    addKey.put(entry.getKey(), null);
                }
            }
            for (Map.Entry<String, Object> entry : image.entrySet()) {
                if (!toTrack.containsKey(entry.getKey())) {
                    delKey.put(entry.getKey(), entry.getValue());
                }
            }
            logUpdate(domain, toTrack, history, update, addKey, delKey, idField, time);
        }
    }

    private void logInsert(DBDomainService<DBDocument> domain, DBDocument toTrack, DBDocument history, String idField, long time) {
        DBList options = history.getDBList(OPTIONS);
        history.put(IMAGE, toTrack);
        options.add(Factory.createDBSortedDocument()
            .append(TIME, time)
            .append(OPTION, OPTION_INSERT)
        );
        domain.save(DBDataFilter.equal(idField, history.get(idField)), history);
        insert++;
    }

    private void logUpdate(DBDomainService<DBDocument> domain, DBDocument toTrack, DBDocument history, DBDocument update, DBDocument addKey, DBDocument delKey, String idField, long time) {
        DBList options = history.getDBList(OPTIONS);
        history.put(IMAGE, toTrack);
        if (!update.isEmpty()) {
            options.add(Factory.createDBSortedDocument()
                .append(TIME, time)
                .append(OPTION, OPTION_UPDATE)
                .append(DETAIL, update)
            );
            this.update++;
        }
        if (!addKey.isEmpty()) {
            options.add(Factory.createDBSortedDocument()
                .append(TIME, time)
                .append(OPTION, OPTION_ADDKEY)
                .append(DETAIL, addKey)
            );
            this.addKey++;
        }
        if (!delKey.isEmpty()) {
            options.add(Factory.createDBSortedDocument()
                .append(TIME, time)
                .append(OPTION, OPTION_DELKEY)
                .append(DETAIL, delKey)
            );
            this.delKey++;
        }
        domain.save(DBDataFilter.equal(idField, history.get(idField)), history);
    }

    public void recovery(long time, DBDomainService<DBDocument> toTrackDomain, DBDomainService<DBDocument> historyDomain, String idField) {
        DBResult<DBDocument> result = historyDomain.cursor(null);
        try (DBCursor<DBDocument> cursor = result.getCursor()) {
            cursor.forEachRemaining(history -> {
                DBDocument image = history.getDBDocument(IMAGE);
                Deque<DBDocument> options = (LinkedList<DBDocument>) history.getDBList(OPTIONS).asList(DBDocument.class);
                boolean changed = false;

                while (!options.isEmpty()) {
                    DBDocument option = options.pollLast();
                    if ((long) option.get(TIME) < time) {
                        break;
                    } else {
                        switch (option.getString(OPTION)) {
                            case OPTION_CREATE:
                            case OPTION_INSERT:
                                image = Factory.createDBSortedDocument();
                                break;
                            case OPTION_DELETE:
                                image = option.getDBDocument(DETAIL);
                                break;
                            case OPTION_UPDATE:
                            case OPTION_DELKEY:
                                for (Map.Entry<String, Object> entry : option.getDBDocument(DETAIL).entrySet()) {
                                    image.put(entry.getKey(), entry.getValue());
                                }
                                break;
                            case OPTION_ADDKEY:
                                for (Map.Entry<String, Object> entry : option.getDBDocument(DETAIL).entrySet()) {
                                    image.remove(entry.getKey());
                                }
                                break;
                            default:
                                throw new RuntimeException("发现未知的操作:" + option.getString(OPTION));
                        }
                        changed = true;
                    }
                }
                if (changed) {
                    if (image.size() > 0) {
                        toTrackDomain.save(DBDataFilter.equal(idField, history.get(idField)), image);
                    } else {
                        toTrackDomain.delete(DBDataFilter.equal(idField, history.get(idField)));
                    }
                }
            });
        }
    }

    public int getCreate() {
        return create;
    }

    public int getInsert() {
        return insert;
    }

    public int getDelete() {
        return delete;
    }

    public int getUpdate() {
        return update;
    }

    public int getAddKey() {
        return addKey;
    }

    public int getDelKey() {
        return delKey;
    }
}
